/*
* "doctothumb" program converts the page(s) of a document into jpeg image files
* 
* Copyright (C) 2013 Alin Dumitrescu
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.

* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
* GNU General Public License for more details.

* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
*
* Distributed under the GNU GPL v2. For full terms see the file Copying or
* License URL at: http://www.gnu.org/licenses/gpl.html GPL version 2
*/

#include <windows.h>
#include "doctothumb.h"
#include "../FreeImage/Dist/FreeImage.h"

extern int volatile MaxThreads;

static unsigned DLL_CALLCONV ReadProc(void *buffer, unsigned size, unsigned count, fi_handle handle) {
    return (unsigned)fread(buffer, size, count, (FILE *)handle);
}
static unsigned DLL_CALLCONV WriteProc(void *buffer, unsigned size, unsigned count, fi_handle handle) {
    return (unsigned)fwrite(buffer, size, count, (FILE *)handle);
}
static int DLL_CALLCONV SeekProc(fi_handle handle, long offset, int origin) {
    return fseek((FILE *)handle, offset, origin);
}
static long DLL_CALLCONV TellProc(fi_handle handle) {
    return ftell((FILE *)handle);
}

static FreeImageIO imgio = { ReadProc, NULL, SeekProc, TellProc};                            //io operations for FreeImage

void IMGProcessStartRender(PCommandRender pRCR)
{
    HANDLE Hnd;
    PCommandExecute pCE = (PCommandExecute)malloc(sizeof(CommandExecute));                   //make room for the task (size of Destination*)
    ZeroMemory(pCE,sizeof(CommandExecute));
    CopyMemory(pCE,pRCR,sizeof(CommandRender));                                              //*POD layout is the same (size of Source)
    Hnd = CreateThread(NULL,0,(LPTHREAD_START_ROUTINE)&IMGThreadedStartRender,pCE,0,NULL);   //thread it to release the message pump
    if(Hnd)
        CloseHandle(Hnd);
    else
        PostResult(-3,pRCR);//post "out of resources"
}

void __stdcall IMGThreadedStartRender(PCommandExecute pCE)
{
    __try
    {
        IMGRenderProcess(pCE);
    }
    __except(EXCEPTION_EXECUTE_HANDLER)
    {
        PostResult(-2,pCE);//post "error exception"
    }
    ExitThread( 0);
}

inline MTSharedDoc* FindFreeImg(MTSharedDoc**List, DWORD ThreadId, IMGDoc* Source)
{
    MTSharedDoc**pItem = List;
    for(int i=0;i<MaxThreads;i++)
    {
        if(!(*pItem))
        {
            *pItem = new MTSharedDoc(Source->Clone(Source));
            (*pItem)->Owned = ThreadId;
            break;
        }
        else
        {
            UINT res = InterlockedCompareExchange(&(*pItem)->Owned,ThreadId,0);
            if (!res)
                break;
            else
                pItem++;
        }
    }
    return pItem ? *pItem : NULL;
}

void IMGRenderProcess(PCommandExecute pCE)
{
    char* RandomFiles = NULL;
    char* OrderedFiles = NULL;
    int buffsize = 0;
    IMGRenderContext rc;
    IMGDoc* Img;
    DWORD ThreadId;
    try
    {
        Img = new IMGDoc(pCE->InFilename);
        if (Img->GetType() == undefined)
            PostResult(0,pCE);                                          //notify parent process about an unknown/unsupported format
        else
            try
            {
                int Page = pCE->Pages;                                  //store the page to be rendered
                pCE->OutputRealSize = 0;
                pCE->Pages = Img->GetCount();                           //count the pages in the tif document
                if((Img->GetType() == single) ||
                  ((Img->GetType() == multi) && (Page > 0)) ||
                  ((Img->GetType() == multi) && (pCE->Pages == 1))) //single page render command
                {
                    pCE->Docs = NULL;
                    OrderedFiles = (char*)malloc(MAX_PATH + 2 + 1); //make room for the output file + CRLF + the end of string NULL char
                    rc.CmdExec = pCE;                               //pass the command
                    rc.Page = Page - 1;                             //pass the page
                    rc.Img = Img;                                   //pass the document
                    rc.CmdExec->Files = OrderedFiles;               //pass the output buffer
                    rc.OwnedPtr = NULL;                             //Not threaded
                    IMGProcessPage(&rc);                            //process the request
                    OrderedFiles[pCE->OutputRealSize ?
                        pCE->OutputRealSize - 2 : 0] = 0;           //terminate the buffer before CRLF
                }
                else
                {
                    buffsize = pCE->Pages * (MAX_PATH + 2 + 1);     //max path of a file + CRLF + the end of string NULL char
                    RandomFiles = (char*)malloc(buffsize);          //make room for the files
                    pCE->Threads = 0;
                    pCE->Files = RandomFiles;                       //set the filenames buffer
                    pCE->OutputRealSize = 0;
                    pCE->Docs=(MTSharedDoc**)malloc(
                        (MaxThreads + 1)*sizeof(MTSharedDoc*));     //make room for the shared objects
                    ZeroMemory(pCE->Docs,(MaxThreads + 1)*sizeof(MTSharedDoc*));
                    PIMGRenderContext pRC = NULL;
                    for(int page = 0; page < pCE->Pages; page++)
                    {
                        while(pCE->Threads>=MaxThreads)                                  //if document has a lot of pages dont
                            Sleep(10);                                                  //kill overall performance (thread starvation)
                        if(!pRC)                                                        //(loop-1)a: see comment on (loop-1)d
                        {
                            pRC = (PIMGRenderContext)malloc(sizeof(IMGRenderContext));  //(loop-1)b: allocate a new memory block for the thread parameter
                            pRC->CmdExec = pCE;                                         //pass the command
                            pRC->Page = page;                                           //pass the page
                        }
                        HANDLE Hnd = CreateThread(NULL,0,
                            (LPTHREAD_START_ROUTINE)&IMGRenderPage, pRC,CREATE_SUSPENDED,&ThreadId); //thread the rendering
                        if(Hnd)
                        {
                            MTSharedDoc* shdoc = FindFreeImg(pCE->Docs,ThreadId,Img);           //pass the first available multipage image document reference
                            pRC->Img = shdoc->IMG();                                            //pass the image
                            pRC->OwnedPtr = &shdoc->Owned;                                      //pass the thread id reference
                            InterlockedIncrement(&pCE->Threads);                                //thread created, increment the thread count
                            ResumeThread(Hnd);                                                  //run it
                            CloseHandle(Hnd);
                            pRC = NULL;                                                         //(loop-1)c: if the thread was created set to NULL and
                        }                                                                       //"go to" (loop-1)b
                        else
                        {                                                                       //(loop-1)d: if the thread was not created
                            Sleep(100);                                                         //take a nap and reuse the already allocated (at (loop-1)b)
                            page--;                                                             //pRC memory block for the next run, and
                        }                                                                       //for the same page/image
                    }
                    while (pCE->Threads>0)                                                      //wait for all threads to end
                        Sleep(10);
                    buffsize = pCE->OutputRealSize + 1;                                         //real buffersize + NULL terminator
                    OrderedFiles = (char*)malloc(buffsize);                                     //make room for the output files
                    char *pSrc, *pDest;
                    pSrc  = RandomFiles;
                    pDest = OrderedFiles;
                    for(int page = 0; page < pCE->Pages; page++)
                    {
                        int len = strlen( pSrc);                                                //length of the text
                        strcpy(pDest,pSrc);                                                     //copy to the destination buffer
                        pDest+=len;                                                             //reposition destination after the CRLF of the source
                        pSrc += (MAX_PATH + 2 + 1);                                             //get the next page
                    }
                    OrderedFiles[buffsize - 1] = 0;                                               //terminate the buffer
                }
        }
        catch(...)
        {
            PostResult(-2,pCE);//post error
        }
        PostResult(1,pCE,OrderedFiles);                                                    //post results to parent application
    }
    catch(...)
    {
        PostResult(-2,pCE);//post error
    }
    if(Img)
        delete Img;
    if(pCE->Docs)
    {
        for(int i=0;i<MaxThreads;i++)
        {
            MTSharedDoc* doc = *((MTSharedDoc**)(pCE->Docs + i));
            if(doc)
                delete doc;
        }
        free((void*)pCE->Docs);
    }
    free((void*)pCE);
    if(RandomFiles)
        free((void*)RandomFiles);
    if(OrderedFiles)
        free((void*)OrderedFiles);
}

void __stdcall IMGRenderPage(PIMGRenderContext pRC)
{
    __try
    {
        IMGProcessPage(pRC);
    }
    __except(EXCEPTION_EXECUTE_HANDLER)
    {
        //Should add a per thread error queue in the next release
    }
    long* pThreads = &pRC->CmdExec->Threads;            //Grab the address of the Threads variable
    free((void*)pRC);                                   //Free the input parameter (allocated in the for-loop)
    InterlockedDecrement(pThreads);                     //Say to the Main thread we're done here
    ExitThread(0);
}

void IMGProcessPage(PIMGRenderContext pRC)
{
    const double IPM = 39.3700787;                      //Inches per meter
    SIZE sz,dpi;
    FIBITMAP *orig = NULL;
    FIBITMAP *img = NULL;
    char Filename[MAX_PATH];
    bool release = false;
    __try{
        orig = pRC->Img->GetPage(pRC->Page);                              //set the source for monopage image documents
        if(!orig) return;                                                 //if the image can not be handled there's nothing we can do
        sz.cx = FreeImage_GetWidth(orig);
        sz.cy = FreeImage_GetHeight(orig);
        if (!(sz.cy && sz.cx)) return;                                    //check for weird images
        dpi.cx = (long)(0.5 + FreeImage_GetDotsPerMeterX(orig) / IPM );   //get the dots per inch value for
        dpi.cy = (long)(0.5 + FreeImage_GetDotsPerMeterY(orig) / IPM );   //both X and Y documents
        dpi.cx = dpi.cx ? dpi.cx : 72;                                    //set 72 dpi by default
        dpi.cy = dpi.cy ? dpi.cy : 72;                                    //set 72 dpi by default
        FIBITMAP *tmpimg;
        int dpimax = max(dpi.cx,dpi.cy);                                  //get max dpi
        if (dpimax > pRC->CmdExec->pContext->Resolution)                  //resize to the min dpi
        {
            int new_width = pRC->CmdExec->pContext->Resolution * sz.cx / dpi.cx;
            int new_height = pRC->CmdExec->pContext->Resolution * sz.cy / dpi.cy;
            img = FreeImage_Rescale(orig,new_width,new_height,FILTER_BILINEAR);
            int dpm = (long)(0.5 + pRC->CmdExec->pContext->Resolution * IPM);
            FreeImage_SetDotsPerMeterX(img,dpm);                          //set the new Dots Per Meter
            FreeImage_SetDotsPerMeterY(img,dpm);                          //value for both X and Y
            release = true;                                               //the image needs unload
        }
        else
            img = orig;                                                   //work on original image
        int bpp = FreeImage_GetBPP(img);                                  //prepare for conversion
        switch(((PIMG_Context)pRC->CmdExec->pContext)->BPP)
        {
        case 1:
            if(bpp != ((PIMG_Context)pRC->CmdExec->pContext)->BPP)        //if needed
            {
                tmpimg = img;
                img = FreeImage_Dither(tmpimg,FID_FS);                    //convert the image to BW, error dither
                if (release)
                    FreeImage_Unload(tmpimg);                             //release the previously allocated image during the scale operation
                else
                    release = true;                                       //release the allocated image
            }
            break;
        case 8:
            if(bpp != ((PIMG_Context)pRC->CmdExec->pContext)->BPP)        //if needed
            {
                tmpimg = img;
                img = FreeImage_ConvertToGreyscale(tmpimg);               //convert the image to Gray
                if (release)
                    FreeImage_Unload(tmpimg);                             //release the previously allocated image during the scale operation
                else
                    release = true;                                       //release the allocated image
            }
            break;
        case 24:
            if(bpp != ((PIMG_Context)pRC->CmdExec->pContext)->BPP)        //if needed
            {
                tmpimg = img;
                img = FreeImage_ConvertTo24Bits(tmpimg);                  //convert the image to RGB
                if (release)
                    FreeImage_Unload(tmpimg);                             //release the previously allocated image during the scale operation
                else
                    release = true;                                       //release the allocated image
            }
            break;
        default:
            if(bpp != ((PIMG_Context)pRC->CmdExec->pContext)->BPP)        //if something is below/above
            {
                tmpimg = img;
                img = FreeImage_ConvertTo24Bits(tmpimg);                  //make sure the image is rendered as RGB
                if (release)
                    FreeImage_Unload(tmpimg);                             //release the previously allocated image during the scale operation
                else
                    release = true;                                       //release the allocated image
            }
        }
        CreateUniqueFile( 8, pRC->CmdExec->OutFolder, Filename);
//        sprintf_s(Filename,MAX_PATH - 1,"%s\\%.6d.jpg",pRC->CmdExec->OutFolder,pRC->Page + 1);    
        FreeImage_Save(FIF_JPEG, img, Filename, pRC->CmdExec->pContext->Quality);
        IMGAddFileToList(pRC->CmdExec,Filename,pRC->Page);
    }
    __finally
    {
        if(release)
            FreeImage_Unload(img);
        if(pRC->OwnedPtr)
            pRC->Img->ReleasePage();
        InterlockedExchange(pRC->OwnedPtr,0);
//        delete pRC->Img;
    }
}

void IMGAddFileToList(PCommandExecute pCE, char* Filename, int Page)
{
    long len = strlen(Filename);
    char* pEntry = pCE->Files;
    pEntry += Page*(MAX_PATH + 2 + 1);                    //position the entry in the output buffer
    strcpy_s(pEntry,MAX_PATH,Filename);                   //just copy to the buffer
    pEntry[len+2] = 0;                                    //put the NULL terminator
    pEntry+=len;
    strcpy(pEntry,"\r\n");                                //add CRFL after the filename
    len+=2;                                               //dont include the NULL string terminator but make room for the CRLF
    InterlockedExchangeAdd(&pCE->OutputRealSize,len);     //increment the output buffer to send to parent application
}

/*******************************************************************************************************************************/

IMGDoc::IMGDoc(char* Filename):Filename(Filename),type(undefined),Hnd(NULL),Img(NULL),lockpage(NULL),fmt(FIF_UNKNOWN),Count(0)
{
    Hnd = fopen(Filename, "rb");                                 //open the image document
    if(Hnd)
        fmt = FreeImage_GetFileTypeFromHandle(&imgio,Hnd);       //get the document type
    if (fmt == FIF_UNKNOWN)
        fmt = FreeImage_GetFIFFromFilename(Filename);            //get the document type using the filename's extension
    type = fmt == FIF_TIFF ? multi :
        fmt == FIF_UNKNOWN ? undefined : single;                 //set the document type as multipage, single page
    switch(type)
    {
    case single:
        Img = FreeImage_LoadFromHandle(fmt,&imgio,Hnd);          //load the image document
        if(Img)
            Count = 1;
        else
            type = undefined;
        break;
    case multi:
        Img = FreeImage_OpenMultiBitmapFromHandle
            (fmt,&imgio,Hnd);                                    //try opening the tif document
        if(Img)
        {
            Count = FreeImage_GetPageCount(
                (FIMULTIBITMAP*)Img);                            //count the pages in the tif document
            if (Count < 1)
            {
                FreeImage_CloseMultiBitmap(
                    (FIMULTIBITMAP*)Img);                        //close the multipage image document
                type = undefined;
            }
        }
        else
            type = undefined;
        break;
    }
    if(Hnd)
        fclose(Hnd);                                             //close handle
    Hnd = NULL;
};
    
IMGDoc::IMGDoc(char* Filename,IMGType type,FREE_IMAGE_FORMAT fmt,int Pages):
    Filename(Filename),type(type),Hnd(NULL),Img(NULL),lockpage(NULL),fmt(fmt),Count(Pages)
{
    Hnd = fopen(Filename, "rb");                                 //open the image document
    if(!Hnd)
        IMGUndefine(this);
    switch(type)
    {
    case single:
        Img = FreeImage_LoadFromHandle(fmt,&imgio,Hnd);          //load the image document
        if(!Img)
            IMGUndefine(this);
        else
            Pages = 1;
        break;
    case multi:
        Img = FreeImage_OpenMultiBitmapFromHandle
            (fmt,&imgio,Hnd);                                    //try opening the tif document
        if(!Img)
            IMGUndefine(this);
        break;
    }
};

IMGDoc::~IMGDoc()
{
    if(type==multi)
    {
        ReleasePage();                                           //if the page is locked release it
        FreeImage_CloseMultiBitmap((FIMULTIBITMAP*)Img);         //close the multipage document
    }
    else
        FreeImage_Unload((FIBITMAP*)Img);                        //close the single page document
    if(Hnd)
        fclose(Hnd);                                             //release the handle
};

IMGDoc* IMGDoc::Clone(IMGDoc* Source)
{
    return new IMGDoc(
        Source->Filename,
        Source->type,
        Source->fmt,
        Source->Count);                                          //return the same object
};

FIBITMAP* IMGDoc::GetPage(int Number)
{
    return
        (type == multi) ?                                               //if multipage
            (lockpage) ?                                                //if page already locked
            lockpage :                                                  //return locked page
            lockpage = FreeImage_LockPage((FIMULTIBITMAP*)Img,Number) : //if not locked, lock - store - return
        Number ?                                                        //if single page and the requested page > 0
            NULL :                                                      //return null
            (FIBITMAP*)Img;                                             //return the image
};

void IMGDoc::ReleasePage()
{
    if (lockpage)                                                       //if page already locked
        FreeImage_UnlockPage((FIMULTIBITMAP*)Img,lockpage,false);
    lockpage = NULL;
};

void IMGDoc::IMGUndefine(PIMGDoc Source)
{
    if(Source->Hnd)
        fclose(Source->Hnd);
    Source->fmt = FIF_UNKNOWN;
    Source->type = undefined;
    Source->Count = 0;
}

int IMGDoc::GetCount()
{
    return Count;
};

IMGType IMGDoc::GetType()
{
    return type;
};